package com.talos.service.codegen;


import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Sets;
import com.talos.dal.dataobject.codegen.CodegenColumnDO;
import com.talos.dal.dataobject.codegen.CodegenTableDO;
import com.talos.enums.codegen.CodegenColumnHtmlTypeEnum;
import com.talos.enums.codegen.CodegenColumnListConditionEnum;
import com.talos.enums.codegen.CodegenSceneEnum;
import com.talos.enums.codegen.CodegenTemplateTypeEnum;
import com.talos.framework.common.constant.SystemConstant;
import com.talos.framework.mybatis.core.dataobject.BaseDO;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.io.FileUtils;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Array;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static cn.hutool.core.text.CharSequenceUtil.*;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static cn.hutool.core.util.RandomUtil.randomInt;

public class MysqlClassMaker {

    private static final Set<String> IGNORES_TABLES = new HashSet<>(Arrays.asList(
            "flyway_schema",
            "flyway_schema_history"
    ));


    /**
     * 字段名与 {@link CodegenColumnListConditionEnum} 的默认映射
     * 注意，字段的匹配以后缀的方式
     */
    private static final Map<String, CodegenColumnListConditionEnum> COLUMN_LIST_OPERATION_CONDITION_MAPPINGS =
            MapUtil.<String, CodegenColumnListConditionEnum>builder()
                    .put("name", CodegenColumnListConditionEnum.LIKE)
                    .put("time", CodegenColumnListConditionEnum.BETWEEN)
                    .put("date", CodegenColumnListConditionEnum.BETWEEN)
                    .build();

    /**
     * 字段名与 {@link CodegenColumnHtmlTypeEnum} 的默认映射
     * 注意，字段的匹配以后缀的方式
     */
    private static final Map<String, CodegenColumnHtmlTypeEnum> COLUMN_HTML_TYPE_MAPPINGS =
            MapUtil.<String, CodegenColumnHtmlTypeEnum>builder()
                    .put("status", CodegenColumnHtmlTypeEnum.RADIO)
                    .put("sex", CodegenColumnHtmlTypeEnum.RADIO)
                    .put("type", CodegenColumnHtmlTypeEnum.SELECT)
                    .put("image", CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD)
                    .put("file", CodegenColumnHtmlTypeEnum.FILE_UPLOAD)
                    .put("content", CodegenColumnHtmlTypeEnum.EDITOR)
                    .put("description", CodegenColumnHtmlTypeEnum.EDITOR)
                    .put("demo", CodegenColumnHtmlTypeEnum.EDITOR)
                    .put("time", CodegenColumnHtmlTypeEnum.DATETIME)
                    .put("date", CodegenColumnHtmlTypeEnum.DATETIME)
                    .build();

    /**
     * 多租户编号的字段名
     */
    public static final String TENANT_ID_FIELD = "tenantId";
    /**
     * {@link } 的字段
     */
    public static final Set<String> BASE_DO_FIELDS = new HashSet<>();
    /**
     * 新增操作，不需要传递的字段
     */
    private static final Set<String> CREATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id");
    /**
     * 修改操作，不需要传递的字段
     */
    private static final Set<String> UPDATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet();
    /**
     * 列表操作的条件，不需要传递的字段
     */
    private static final Set<String> LIST_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id");
    /**
     * 列表操作的结果，不需要返回的字段
     */
    private static final Set<String> LIST_OPERATION_RESULT_EXCLUDE_COLUMN = Sets.newHashSet();

    static {
        Arrays.stream(ReflectUtil.getFields(BaseDO.class)).forEach(field -> BASE_DO_FIELDS.add(field.getName()));
        BASE_DO_FIELDS.add(TENANT_ID_FIELD);
        // 处理 OPERATION 相关的字段
        CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS);
        UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS);
        //LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS);
        LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间，还是可能需要传递的
        //LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS);
        LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); // 创建时间，还是需要返回的
    }

    private static final Map<String, String> JAVA_TYPES = new HashMap<>();

    private static final Map<String, String> TYPE_IMPORTS = new HashMap<>();

    static {
        // 数值类型
        JAVA_TYPES.put("tinyint", "Integer");
        JAVA_TYPES.put("smallint", "Integer");
        JAVA_TYPES.put("mediumint", "Integer");
        JAVA_TYPES.put("int", "Integer");
        JAVA_TYPES.put("bigint", "Long");
        JAVA_TYPES.put("float", "BigDecimal");
        JAVA_TYPES.put("double", "BigDecimal");
        JAVA_TYPES.put("decimal", "BigDecimal");

        // 字符串类型
        JAVA_TYPES.put("char", "String");
        JAVA_TYPES.put("varchar", "String");
        JAVA_TYPES.put("text", "String");
        JAVA_TYPES.put("longtext", "String");

        // 日期类型
        JAVA_TYPES.put("date", "LocalDate");
        JAVA_TYPES.put("time", "LocalTime");
        JAVA_TYPES.put("datetime", "LocalDateTime");
        JAVA_TYPES.put("timestamp", "LocalDateTime");

        TYPE_IMPORTS.put("BigDecimal", "import java.math.BigDecimal;");
        TYPE_IMPORTS.put("LocalDate", "import java.time.LocalDate;");
        TYPE_IMPORTS.put("LocalTime", "import java.time.LocalTime;");
        TYPE_IMPORTS.put("LocalDateTime", "import java.time.LocalDateTime;");
    }

    private final String JAVA_DIR = GeneratorUtils.joinPathsBoth("src", "main", "java");

    private final String host;
    private final String database;
    private final String username;
    private final String password;
    private final Connection connection;

    public MysqlClassMaker(String host, String database, String username, String password) {
        this.host = host;
        this.database = database;
        this.username = username;
        this.password = password;
        this.connection = this.getConnection();
    }

    public void generic(String path, String projectName,String moduleName, String basePackage, String... names) {
        this.generic(true, path,projectName,moduleName, basePackage, names);
    }

    public void generic(boolean api, String projectPath,String projectName, String moduleName, String basePackage, String... names) {
        System.out.println("接口初始化开始");
        List<Table> tables = new ArrayList<>();
        List<CodegenTableDO> tableDOList = new ArrayList<>();

        try {
            Map<String, String> tableNames = this.getTables(names);
            for (Map.Entry<String, String> tableEntry : tableNames.entrySet()) {
                List<Column> columns = this.getColumns(tableEntry.getKey());
                tables.add(new Table(api, tableEntry.getKey(), tableEntry.getValue(), columns));
            }
        } finally {
            this.closeConnection();
        }

        for (Table table : tables) {

            CodegenTableDO tableDO = new CodegenTableDO();

            String tableName = table.getTableName().toLowerCase();

            tableDO.setProjectModuleName(projectName);
            tableDO.setModuleName(moduleName);
            tableDO.setTableName(table.getTableName());
            tableDO.setTableComment(table.getComment());
            tableDO.setClassComment(table.getComment());
            tableDO.setScene(CodegenSceneEnum.ADMIN.getScene());

            tableDO.setClassName(upperFirst(toCamelCase(tableName)));
            tableDO.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase());
            tableDO.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());

            List<CodegenColumnDO> columns = new ArrayList<>();

            int index = 1;
            for (Column column : table.getColumns()) {
                CodegenColumnDO tableColumn = new CodegenColumnDO();
                tableColumn.setColumnName(column.getName());
                tableColumn.setColumnComment(column.getComment());
                tableColumn.setDataType(column.getType());
                if(column.getPrimaryKey().equalsIgnoreCase("PRI")){
                    tableColumn.setPrimaryKey(true);
                }else {
                    tableColumn.setPrimaryKey(false);
                }
                tableColumn.setJavaType(this.getJavaType(column.getType()));
                tableColumn.setJavaField(this.snakeToCamel(column.getName()));
                tableColumn.setNullable(column.isNullable());
                tableColumn.setOrdinalPosition(index++);

                processColumnOperation(tableColumn); // 处理 CRUD 相关的字段的默认值
                //processColumnUI(column); // 处理 UI 相关的字段的默认值
                processColumnExample(tableColumn); // 处理字段的 swagger example 示例

                columns.add(tableColumn);

            }

            CodegenEngine engine = new CodegenEngine();
            Map<String, String> result =  engine.execute(basePackage, tableDO, columns, null, null);
            result.forEach((path,content)-> {
                try {
                    FileUtils.writeStringToFile(new File(path), content, "UTF-8");
                } catch (IOException e) {
                    System.out.println("生成代异常");
                }
            } );

        }


        System.out.println("接口初始化结束");
    }


    private void processColumnOperation(CodegenColumnDO column) {
        // 处理 createOperation 字段
        column.setCreateOperation(!CREATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField())
                && !column.getPrimaryKey()); // 对于主键，创建时无需传递
        // 处理 updateOperation 字段
        column.setUpdateOperation(!UPDATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField())
                || column.getPrimaryKey()); // 对于主键，更新时需要传递
        // 处理 listOperation 字段
        column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField())
                && !column.getPrimaryKey()); // 对于主键，列表过滤不需要传递
        // 处理 listOperationCondition 字段
        COLUMN_LIST_OPERATION_CONDITION_MAPPINGS.entrySet().stream()
                .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey()))
                .findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition()));
        if (column.getListOperationCondition() == null) {
            column.setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition());
        }
        // 处理 listOperationResult 字段
        column.setListOperationResult(!LIST_OPERATION_RESULT_EXCLUDE_COLUMN.contains(column.getJavaField()));
    }

    private void processColumnUI(CodegenColumnDO column) {
        // 基于后缀进行匹配
        COLUMN_HTML_TYPE_MAPPINGS.entrySet().stream()
                .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey()))
                .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType()));
        // 如果是 Boolean 类型时，设置为 radio 类型.
        if (Boolean.class.getSimpleName().equals(column.getJavaType())) {
            column.setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType());
        }
        // 如果是 LocalDateTime 类型，则设置为 datetime 类型
        if (LocalDateTime.class.getSimpleName().equals(column.getJavaType())) {
            column.setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
        }
        // 兜底，设置默认为 input 类型
        if (column.getHtmlType() == null) {
            column.setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
        }
    }

    /**
     * 处理字段的 swagger example 示例
     *
     * @param column 字段
     */
    private void processColumnExample(CodegenColumnDO column) {
        // id、price、count 等可能是整数的后缀
        if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "id", "price", "count")) {
            column.setExample(String.valueOf(randomInt(1, Short.MAX_VALUE)));
            return;
        }
        // name
        if (StrUtil.endWithIgnoreCase(column.getJavaField(), "name")) {
            column.setExample(randomEle(new String[]{"张三", "李四", "王五", "赵六", "芋艿"}));
            return;
        }
        // status
        if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "status", "type")) {
            column.setExample(randomEle(new String[]{"1", "2"}));
            return;
        }
        // url
        if (StrUtil.endWithIgnoreCase(column.getColumnName(), "url")) {
            column.setExample("https://www.iocoder.cn");
            return;
        }
        // reason
        if (StrUtil.endWithIgnoreCase(column.getColumnName(), "reason")) {
            column.setExample(randomEle(new String[]{"不喜欢", "不对", "不好", "不香"}));
            return;
        }
        // description、memo、remark
        if (StrUtil.endWithAnyIgnoreCase(column.getColumnName(), "description", "memo", "remark")) {
            column.setExample(randomEle(new String[]{"你猜", "随便", "你说的对"}));
            return;
        }
    }

    private String getModelColumnString(Column column, Set<String> imports) {
        String type = this.getJavaType(column.getType());

        if (TYPE_IMPORTS.containsKey(type)) {
            imports.add(TYPE_IMPORTS.get(type));
        }

        return String.format(
                "\n    @TableField(value = \"`%s`\")\n    @ApiModelProperty(value = \"%s\")\n    private %s %s;\n",
                column.getName(),
                column.getComment(),
                type,
                this.snakeToCamel(column.getName())
        );
    }

    private String getQueryColumnString(Column column, Set<String> imports) {
        String type = this.getJavaType(column.getType());

        if (TYPE_IMPORTS.containsKey(type)) {
            imports.add(TYPE_IMPORTS.get(type));
        }

        return String.format(
                "\n    @FieldSearch(value = \"`%s`\")\n    @ApiModelProperty(value = \"%s\")\n    private %s %s;\n",
                column.getName(),
                column.getComment(),
                type,
                this.snakeToCamel(column.getName())
        );
    }

    private String getDtoColumnString(Column column, Set<String> imports) {
        String type = this.getJavaType(column.getType());

        if (TYPE_IMPORTS.containsKey(type)) {
            imports.add(TYPE_IMPORTS.get(type));
        }

        return String.format(
                "\n    @ApiModelProperty(value = \"%s\")\n    private %s %s;\n",
                column.getComment(),
                type,
                this.snakeToCamel(column.getName())
        );
    }

    private String snakeToCamel(String snakeCase) {
        Pattern pattern = Pattern.compile("_(\\w)");
        Matcher matcher = pattern.matcher(snakeCase);
        StringBuffer camelCase = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(camelCase, matcher.group(1).toUpperCase());
        }

        matcher.appendTail(camelCase);
        return camelCase.toString();
    }

    private String getFilePath(File dir, String className) {
        return GeneratorUtils.joinPaths(dir.getAbsolutePath(), className + ".java");
    }

    private Connection getConnection() {
        try {
            // 加载数据库驱动
            Class.forName("com.mysql.cj.jdbc.Driver");

            String url = String.format(
                    "jdbc:mysql://%s/%s",
                    this.host,
                    this.database
            );

            return DriverManager.getConnection(url, this.username, this.password);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    private void closeConnection() {
        try {
            this.connection.close();
        } catch (SQLException ignore) {

        }
    }

    private List<Column> getColumns(String tableName) {
        List<Column> columns = new ArrayList<>();

        Statement statement = this.getStatement();

        ResultSet resultSet;

        try {
            resultSet = statement.executeQuery(this.getColumnsSql(tableName));
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }

        try {
            while (resultSet.next()) {
                String name = resultSet.getString(1);

                if (SystemConstant.IGNORES_COLUMNS.contains(name)) {
                    continue;
                }

                String type = resultSet.getString(2);
                String comment = resultSet.getString(3);
                String primaryKey = resultSet.getString(4);
                String nullable = resultSet.getString(5);

                columns.add(new Column(name, type, comment,primaryKey,nullable));
            }
        } catch (SQLException e) {
            this.closeStatement(statement);

            throw new RuntimeException(e);
        } finally {
            this.closeResultSet(resultSet);
            this.closeStatement(statement);
        }

        return columns;
    }

    private String getColumnsSql(String tableName) {
        return String.format(
                "SELECT `COLUMN_NAME`, `DATA_TYPE`, `COLUMN_COMMENT`,COLUMN_KEY,IS_NULLABLE " +
                        "FROM `INFORMATION_SCHEMA`.`COLUMNS` " +
                        "WHERE `TABLE_NAME` = '%s' " +
                        "AND `TABLE_SCHEMA` = '%s' " +
                        "ORDER BY `ORDINAL_POSITION`",
                tableName,
                this.database
        );
    }

    private Map<String, String> getTables(String... names) {
        Map<String, String> tables = new HashMap<>();

        Statement statement = this.getStatement();

        ResultSet resultSet;

        try {
            resultSet = statement.executeQuery(this.getTableSql(names));
        } catch (SQLException e) {
            this.closeStatement(statement);

            throw new RuntimeException(e);
        }

        try {
            while (resultSet.next()) {
                String tableName = resultSet.getString(1);

                if (IGNORES_TABLES.contains(tableName)) {
                    continue;
                }

                tables.put(tableName, resultSet.getString(2));
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            this.closeResultSet(resultSet);
            this.closeStatement(statement);
        }

        return tables;
    }

    private String getTableSql(String... names) {
        String sql = String.format(
                "SELECT `TABLE_NAME`, `TABLE_COMMENT` " +
                        "FROM `INFORMATION_SCHEMA`.`TABLES` " +
                        "WHERE `TABLE_SCHEMA` = '%s'",
                this.database
        );

        if (Array.getLength(names) > 0) {
            sql += " AND `TABLE_NAME` IN ('" + String.join("', '", names) + "')";
        }

        return sql;
    }

    private void closeResultSet(ResultSet resultSet) {
        try {
            resultSet.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private Statement getStatement() {
        try {
            return this.connection.createStatement();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private void closeStatement(Statement statement) {
        try {
            statement.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public String getJavaType(String type) {
        if (!JAVA_TYPES.containsKey(type)) {
            return "String";
        }

        return JAVA_TYPES.get(type);
    }

    private void writeFileIfNotExists(String target, String content) {
        if ((new File(target)).isFile()) {
            System.out.printf("跳过已存在文件:%s", target);
            System.out.println();

            return;
        }

        System.out.printf("写入内容:%s", target);
        System.out.println();

        try {
            BufferedWriter writer = new BufferedWriter(new FileWriter(target));

            writer.write(content);

            writer.close();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private void mkdir(File file) {
        if (!file.exists() && !file.mkdirs()) {
            throw new RuntimeException(String.format("文件创建失败:{%s}", file.getAbsoluteFile()));
        }
    }

    @Getter
    @ToString
    public static class Table {

        private final boolean relation;

        private final String tableName;

        private final String fileName;

        private final String variableName;

        private final String packageName;

        private final String pathName;

        private final String urlName;

        private final String comment;

        private final List<Column> columns;

        public Table(boolean api, String tableName, String comment, List<Column> columns) {
            if (!api) {
                this.relation = true;
            } else {
                this.relation = tableName.endsWith("_relation");
            }

            if (this.relation) {
                this.tableName = tableName.replace("_relation", "");
            } else {
                this.tableName = tableName;
            }

            this.fileName = Arrays.stream(this.tableName.split("_"))
                    .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
                    .collect(Collectors.joining());

            this.variableName = this.fileName.substring(0, 1).toLowerCase() + this.fileName.substring(1);
            this.packageName = this.tableName.toLowerCase().replaceAll("_", ".");
            this.pathName = this.tableName.toLowerCase().replaceAll("_", Matcher.quoteReplacement(File.separator));
            this.urlName = this.tableName.toLowerCase().replaceAll("_", "-");
            this.comment = comment;
            this.columns = columns;
        }

    }

    @Getter
    @ToString
    public static class Column {

        private final String name;
        private final String type;
        private final String comment;
        private final String primaryKey;
        private final boolean nullable;

        public Column(String name, String type, String comment, String primaryKey,String nullable) {
            this.name = name;
            this.type = type;
            this.comment = comment;
            this.primaryKey= primaryKey;
            this.nullable = "YES".equals(nullable) ? true : false;
        }

    }

}
